昨天遇到了頁面間資料交換的問題,所以今天我們要來在app裡面建立跨頁面的資料分享機制:
原本的頁面關係
在MainPage
點選AstroPicture
的愛心按鈕之後,要將資料交給FavoritePage
並可以導頁到收藏細節頁
這次會使用官方推薦的Provider
套件來解決這個問題
好的,那我們就開始吧!
在pubspec.yaml
內加入provider
並下載套件:
$ flutter pub add provider
$ flutter pub get
provider
的使用分為幾個階段:
ChangeNotifierProvider
),放在想要傳遞資料以及使用資料的組件(在專案中是指MainPage
, FavoritePage
)的上層。ChangeNotifier
),也就是我們這次指稱的app state,供各組件訂閱使用(Comsumer
)或操作(Provider.of
)。ChangeNotifier
可以通知訂閱此資料模型的組件,依照最新的資料重繪。provider
的底層實踐自Inherited Widget
,可以和子組件共享資料,並在資料變化時通知使用該資料的子組件重繪,詳見 InheritedWidget
ChangeNotifier
,以官方提供的購物車資料模型為例:
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
UnmodifiableListView<Item> get items => UnmodifiableListView(_items);
int get totalPrice => _items.length * 42;
void add(Item item) {
_items.add(item);
notifyListeners(); // 通知訂閱CartModel的子組件資料已變更,需重繪
}
void removeAll() {
_items.clear();
notifyListeners(); // 通知訂閱CartModel的子組件資料已變更,需重繪
}
}
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CartModel(),
child: const MyApp(), // 在MyApp class之中可以透過Consumer來訂閱CartModel實例,或者Provider.of來使用CartModel內部的Method
),
);
}
MultiProvider
:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => CartModel()), // ChangeNotifierProvider具備在資料變更時通知組件重繪的功能
Provider(create: (context) => SomeOtherClass()), // Provider只負責取data model裡面的值,不會觸發組件重繪
],
child: const MyApp(), // MyApp之中可以取用兩種data model實例的資料
),
);
}
Provider
/ChangeNotifierProvider
提供的資料MyApp
外層包了ChangeNotifierProvider
,在MyApp繪製UI時我可以建立Comsumer
取得CartModel()
實例內部的值
// MyApp.dart
@override
Widget build(BuildContext context){
return HumongousWidget(
child: AnotherMonstrousWidget(
child: Consumer<CartModel>( // 定義Consumer,取得上層傳下來的CartModel()實例
builder: (context, cart, child) { // 在第二個parameter將該實例指稱為cart
return Text('Total price: ${cart.totalPrice}'); // 取用cart內部的totalPrice值
},
),
),
);
}
如上,若是組件使用的值被變更時,有註明要notifyListener()
,則會觸發該組件的重繪Consumer
包覆的子組件全部都會重繪。由於子組件使用的結構愈繁複、層級越多,重繪的資源消耗就會比較大,因此若要增進效能,最好將Consumer
包在較低層的組件上
// better: 只觸發Text重繪
WidgetA(
child: WidgetB(
child: WidgetC(
child: Consumer<DataModel>(
builder: (context, dataModel, child) {
return Text(dataModel.value);
}
)
)
)
)
// worse: 觸發WidgetA, WidgetB, WidgetC, Text重繪
Consumer<DataModel>(
builder: (context, dataModel, child) {
return WidgetA(
child: WidgetB(
child: WidgetC(
child: Text(dataModel.value)
)
)
)
}
)
Provider.of
:
class _MyAppState extends State<MyApp> {
void _cleanCart(BuildContext context){
// 只是要調用「清除購物車中所有的項目」方法,並沒有要取用購物車中的資料
Provider.of<CartModel>(context, listen: false).removeAll();
}
@override
Widget build(BuildContext context){
return TextButton(
onPress(){
_cleanCart(context);
},
child: Text('Clean Cart!')
)
}
}
今天我們看到了如何使用Provider
套件來實現組件間的溝通,幾個關鍵的元素分別為
ChangeNotifier
:讓資料模型在內部資料變更時,能夠通知有訂閱的組件ChangeNotifierProvider
:放在最上層,建立資料模型的實例供子組件取用Consumer
: 讓子組件能夠取得資料Provider.of
: 不取用資料,只調用資料模型提供的方法在App愈上層的地方加入ChangeNotifierProvider
,能共享資料模型的組件的範圍就愈大,但是同樣要傳愈多資料給其他根本用不到這些資料的組件。
明天要來為APP加入第三方套件:table_calendar,開始刻畫我們的月曆頁囉!